package org.imgscalr;

import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;

/**
 * @author jiangli
 */
public class Scalr {

    /**
     * 是否开启debug模式系统变量名
     */
    public static final String DEBUG_PROPERTY_NAME = "imgscalr.debug";

    /**
     * debug模式，日志前缀系统变量名
     */
    public static final String LOG_PREFIX_PROPERTY_NAME = "imgscalr.logPrefix";

    public static final boolean DEBUG = Boolean.getBoolean(DEBUG_PROPERTY_NAME);

    public static final String LOG_PREFIX = System.getProperty(LOG_PREFIX_PROPERTY_NAME, "[imgscalr] ");

    /**
     * 算法阈值，当像素<=800时，采用质量优先算法；当像素在(800,1600]之间时，采用平衡算法；当像素
     * 大于1600时，采用速度优先算法。
     */
    public static final int THRESHOLD_BALANCED_SPEED = 1600;

    /**
     * 算法阈值，当像素<=800时，采用质量优先算法；当像素在(800,1600]之间时，采用平衡算法；当像素
     * 大于1600时，采用速度优先算法。
     */
    public static final int THRESHOLD_QUALITY_BALANCED = 800;

    public static final ConvolveOp OP_ANTIALIAS = new ConvolveOp(
            new Kernel(3, 3, new float[] { .0f, .08f, .0f, .08f, .68f, .08f,
                    .0f, .08f, .0f }), ConvolveOp.EDGE_NO_OP, null);


    public static final RescaleOp OP_DARKER = new RescaleOp(0.9f, 0, null);

    public static final RescaleOp OP_BRIGHTER = new RescaleOp(1.1f, 0, null);

    public static final ColorConvertOp OP_GRAYSCALE = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);

    static {
        log(0, "Debug output ENABLED");
    }

    public static BufferedImage apply(BufferedImage src, BufferedImageOp... ops) {
        long t = -1;
        if (DEBUG) {
            t = System.currentTimeMillis();
        }
        if (null == src) {
            throw new IllegalArgumentException("src cannot be null");
        }
        if (null == ops || ops.length == 0) {
            throw new IllegalArgumentException("ops cannot be null or empty");
        }
        int type = src.getType();
        if (!(type == BufferedImage.TYPE_INT_RGB || type == BufferedImage.TYPE_INT_ARGB)) {
            src = copyToOptimalImage(src);
        }

        if (DEBUG) {
            log(0, "Applying %d BufferedImageOps...", ops.length);
        }

        boolean hasReassignedSrc = false;

        for (BufferedImageOp op : ops) {
            long subT = -1;
            if (DEBUG) {
                subT = System.currentTimeMillis();
            }

            // Skip null ops instead of throwing an exception.
            if (op == null) {
                continue;
            }

            if (DEBUG) {
                log(1, "Applying BufferedImageOp [class=%s, toString=%s]...", op.getClass(), op.toString());
            }

            /*
             * Must use op.getBounds instead of src.getWidth and src.getHeight
             * because we are trying to create an image big enough to hold the
             * result of this operation (which may be to scale the image
             * smaller), in that case the bounds reported by this op and the
             * bounds reported by the source image will be different.
             */
            Rectangle2D resultBounds = op.getBounds2D(src);

            // Watch out for flaky/misbehaving ops that fail to work right.
            if (resultBounds == null) {
                throw new ImagingOpException("BufferedImageOp [" + op.toString() + "] getBounds2D(src) returned null " +
                        "bounds for the target image; this should not happen and indicates a problem with application of this type of op.");
            }

            /*
             * We must manually create the target image; we cannot rely on the
             * null-destination filter() method to create a valid destination
             * for us thanks to this JDK bug that has been filed for almost a
             * decade:
             * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4965606
             */
            BufferedImage dest = createOptimalImage(src,
                    (int) Math.round(resultBounds.getWidth()),
                    (int) Math.round(resultBounds.getHeight()));

            // Perform the operation, update our result to return.
            BufferedImage result = op.filter(src, dest);

            /*
             * Flush the 'src' image ONLY IF it is one of our interim temporary
             * images being used when applying 2 or more operations back to
             * back. We never want to flush the original image passed in.
             */
            if (hasReassignedSrc) {
                src.flush();
            }

            /*
             * Incase there are more operations to perform, update what we
             * consider the 'src' reference to our last result so on the next
             * iteration the next op is applied to this result and not back
             * against the original src passed in.
             */
            src = result;

            /*
             * Keep track of when we re-assign 'src' to an interim temporary
             * image, so we know when we can explicitly flush it and clean up
             * references on future iterations.
             */
            hasReassignedSrc = true;

            if (DEBUG) {
                log(1, "Applied BufferedImageOp in %d ms, result [width=%d, height=%d]",
                        System.currentTimeMillis() - subT, result.getWidth(), result.getHeight());
            }

        }

        if (DEBUG) {
            log(0, "All %d BufferedImageOps applied in %d ms", ops.length, System.currentTimeMillis() - t);
        }

        return src;
    }

    public static BufferedImage crop(BufferedImage src, int width, int height, BufferedImageOp... ops) {
        return crop(src, 0, 0, width, height, ops);
    }

    public static BufferedImage crop(BufferedImage src, int x, int y, int width, int height, BufferedImageOp... ops) {
        long t = -1;
        if (DEBUG) {
            t = System.currentTimeMillis();
        }
        if (null == src) {
            throw new IllegalArgumentException("src cannot be null");
        }
        if (x < 0 || y < 0 || width < 0 || height < 0) {
            throw new IllegalArgumentException("Invalid crop bounds: x [" + x + "], y [" + y +
                    "], width [" + width + "] and height [" + height + "] must all be >= 0");
        }
        int srcWidth = src.getWidth();
        int srcHeight = src.getHeight();
        if ((x + width) > srcWidth) {
            throw new IllegalArgumentException("Invalid crop bounds: x + width [" + (x + width)
                    + "] must be <= src.getWidth() [" + srcWidth + "]");
        }
        if ((y + height) > srcHeight) {
            throw new IllegalArgumentException("Invalid crop bounds: y + height [" + (y + height)
                    + "] must be <= src.getHeight() [" + srcHeight + "]");
        }
        if (DEBUG) {
            log(0, "Cropping Image [width=%d, height=%d] to [x=%d, y=%d, width=%d, height=%d]...",
                    srcWidth, srcHeight, x, y, width, height);
        }
        // Create a target image of an optimal type to render into.
        BufferedImage result = createOptimalImage(src, width, height);
        Graphics g = result.getGraphics();

        /*
         * Render the region specified by our crop bounds from the src image
         * directly into our result image (which is the exact size of the crop
         * region).
         */
        g.drawImage(src, 0, 0, width, height, x, y, (x + width), (y + height), null);
        g.dispose();

        if (DEBUG) {
            log(0, "Cropped Image in %d ms", System.currentTimeMillis() - t);
        }

        // Apply any optional operations (if specified).
        if (ops != null && ops.length > 0) {
            result = apply(result, ops);
        }

        return result;
    }

    public static BufferedImage pad(BufferedImage src, int padding, BufferedImageOp... ops) {
        return pad(src, padding, Color.BLACK);
    }

    public static BufferedImage pad(BufferedImage src, int padding, Color color, BufferedImageOp... ops) {
        long t = -1;
        if (DEBUG) {
            t = System.currentTimeMillis();
        }
        if (null == src) {
            throw new IllegalArgumentException("src cannot be null");
        }
        if (padding < 1) {
            throw new IllegalArgumentException("padding [" + padding + "] must be > 0");
        }
        if (null == color) {
            throw new IllegalArgumentException("color cannot be null");
        }
        int srcWidth = src.getWidth();
        int srcHeight = src.getHeight();
        /*
         * Double the padding to account for all sides of the image. More
         * specifically, if padding is "1" we add 2 pixels to width and 2 to
         * height, so we have 1 new pixel of padding all the way around our
         * image.
         */
        int sizeDiff = (padding * 2);
        int newWidth = srcWidth + sizeDiff;
        int newHeight = srcHeight + sizeDiff;
        if (DEBUG) {
            log(0, "Padding Image from [originalWidth=%d, originalHeight=%d, padding=%d] to [newWidth=%d, newHeight=%d]...",
                    srcWidth, srcHeight, padding, newWidth, newHeight);
        }
        boolean colorHasAlpha = (color.getAlpha() != 255);
        boolean imageHasAlpha = (src.getTransparency() != BufferedImage.OPAQUE);
        BufferedImage result;
        /*
         * We need to make sure our resulting image that we render into contains
         * alpha if either our original image OR the padding color we are using
         * contain it.
         */
        if (colorHasAlpha || imageHasAlpha) {
            if (DEBUG) {
                log(1, "Transparency FOUND in source image or color, using ARGB image type...");
            }
            result = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
        } else {
            if (DEBUG) {
                log(1, "Transparency NOT FOUND in source image or color, using RGB image type...");
            }
            result = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
        }

        Graphics g = result.getGraphics();

        // Draw the border of the image in the color specified.
        g.setColor(color);
        g.fillRect(0, 0, newWidth, padding);
        g.fillRect(0, padding, padding, newHeight);
        g.fillRect(padding, newHeight - padding, newWidth, newHeight);
        g.fillRect(newWidth - padding, padding, newWidth, newHeight - padding);

        // Draw the image into the center of the new padded image.
        g.drawImage(src, padding, padding, null);
        g.dispose();

        if (DEBUG) {
            log(0, "Padding Applied in %d ms", System.currentTimeMillis() - t);
        }

        // Apply any optional operations (if specified).
        if (ops != null && ops.length > 0) {
            result = apply(result, ops);
        }

        return result;
    }

    public static BufferedImage resize(BufferedImage src, int targetSize, BufferedImageOp... ops) {
        return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetSize, targetSize, ops);
    }

    public static BufferedImage resize(BufferedImage src, Method scalingMethod, int targetSize, BufferedImageOp... ops) {
        return resize(src, scalingMethod, Mode.AUTOMATIC, targetSize, targetSize, ops);
    }

    public static BufferedImage resize(BufferedImage src, Mode resizeMode, int targetSize, BufferedImageOp... ops) {
        return resize(src, Method.AUTOMATIC, resizeMode, targetSize, targetSize, ops);
    }

    public static BufferedImage resize(BufferedImage src, Method scalingMethod, Mode resizeMode,
                                       int targetSize, BufferedImageOp... ops) {
        return resize(src, scalingMethod, resizeMode, targetSize, targetSize, ops);
    }

    public static BufferedImage resize(BufferedImage src, int targetWidth, int targetHeight, BufferedImageOp... ops) {
        return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetWidth, targetHeight, ops);
    }

    public static BufferedImage resize(BufferedImage src, Method scalingMethod, int targetWidth,
                                       int targetHeight, BufferedImageOp... ops) {
        return resize(src, scalingMethod, Mode.AUTOMATIC, targetWidth, targetHeight, ops);
    }

    public static BufferedImage resize(BufferedImage src, Mode resizeMode, int targetWidth,
                                       int targetHeight, BufferedImageOp... ops) {
        return resize(src, Method.AUTOMATIC, resizeMode, targetWidth, targetHeight, ops);
    }

    public static BufferedImage resize(BufferedImage src, Method scalingMethod, Mode resizeMode, int targetWidth,
                                       int targetHeight, BufferedImageOp... ops) {
        long t = -1;
        if (DEBUG) {
            t = System.currentTimeMillis();
        }
        if (null == src) {
            throw new IllegalArgumentException("src cannot be null");
        }
        if (targetWidth < 0) {
            throw new IllegalArgumentException("targetWidth must be >= 0");
        }
        if (targetHeight < 0) {
            throw new IllegalArgumentException("targetHeight must be >= 0");
        }
        if (scalingMethod == null) {
            throw new IllegalArgumentException("scalingMethod cannot be null. A good default value is Method.AUTOMATIC.");
        }
        if (resizeMode == null) {
            throw new IllegalArgumentException("resizeMode cannot be null. A good default value is Mode.AUTOMATIC.");
        }
        BufferedImage result = null;

        int currentWidth = src.getWidth();
        int currentHeight = src.getHeight();

        // <= 1 is a square or landscape-oriented image, > 1 is a portrait.
        float ratio = ((float) currentHeight / (float) currentWidth);

        if (DEBUG) {
            log(0, "Resizing Image [size=%dx%d, resizeMode=%s, orientation=%s, ratio(H/W)=%f] to [targetSize=%dx%d]",
                    currentWidth, currentHeight, resizeMode, (ratio <= 1 ? "Landscape/Square" : "Portrait"), ratio,
                    targetWidth, targetHeight);
        }
        if (resizeMode == Mode.FIT_EXACT) {
            if (DEBUG) {
                log(1, "Resize Mode FIT_EXACT used, no width/height checking or re-calculation will be done.");
            }
        } else if (resizeMode == Mode.BEST_FIT_BOTH) {
            float requestedHeightScaling = ((float) targetHeight / (float) currentHeight);
            float requestedWidthScaling = ((float) targetWidth / (float) currentWidth);
            float actualScaling = Math.min(requestedHeightScaling, requestedWidthScaling);

            targetHeight = Math.round((float) currentHeight * actualScaling);
            targetWidth = Math.round((float) currentWidth * actualScaling);

            if (targetHeight == currentHeight && targetWidth == currentWidth) {
                return src;
            }
            if (DEBUG) {
                log(1, "Auto-Corrected width and height based on scalingRatio %d.", actualScaling);
            }
        } else {
            if ((ratio <= 1 && resizeMode == Mode.AUTOMATIC) || (resizeMode == Mode.FIT_TO_WIDTH)) {
                if (targetWidth == src.getWidth()) {
                    return src;
                }
                int originalTargetHeight = targetHeight;
                targetHeight = (int) Math.ceil((float) targetWidth * ratio);
                if (DEBUG && originalTargetHeight != targetHeight) {
                    log(1, "Auto-Corrected targetHeight [from=%d to=%d] to honor image proportions.",
                            originalTargetHeight, targetHeight);
                }
            } else {
                if (targetHeight == src.getHeight()) {
                    return src;
                }
                int originalTargetWidth = targetWidth;
                targetWidth = Math.round((float) targetHeight / ratio);
                if (DEBUG && originalTargetWidth != targetWidth) {
                    log(1, "Auto-Corrected targetWidth [from=%d to=%d] to honor image proportions.",
                            originalTargetWidth, targetWidth);
                }
            }
        }

        if (scalingMethod == Method.AUTOMATIC) {
            scalingMethod = determineScalingMethod(targetWidth, targetHeight, ratio);
        }

        if (DEBUG) {
            log(1, "Using Scaling Method: %s", scalingMethod);
        }
        // Now we scale the image
        if (scalingMethod == Method.SPEED) {
            result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
        } else if (scalingMethod == Method.BALANCED) {
            result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        } else if (scalingMethod == Method.QUALITY || scalingMethod == Method.ULTRA_QUALITY) {
            /*
             * If we are scaling up (in either width or height - since we know
             * the image will stay proportional we just check if either are
             * being scaled up), directly using a single BICUBIC will give us
             * better results then using Chris Campbell's incremental scaling
             * operation (and take a lot less time).
             *
             * If we are scaling down, we must use the incremental scaling
             * algorithm for the best result.
             */
            if (targetWidth > currentWidth || targetHeight > currentHeight) {
                if (DEBUG) {
                    log(1, "QUALITY scale-up, a single BICUBIC scale operation will be used...");
                }
                /*
                 * BILINEAR and BICUBIC look similar the smaller the scale jump
                 * upwards is, if the scale is larger BICUBIC looks sharper and
                 * less fuzzy. But most importantly we have to use BICUBIC to
                 * match the contract of the QUALITY rendering scalingMethod.
                 * This note is just here for anyone reading the code and
                 * wondering how they can speed their own calls up.
                 */
                result = scaleImage(src, targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            } else {
                if (DEBUG) {
                    log(1, "QUALITY scale-down, incremental scaling will be used...");
                }
                /*
                 * Originally we wanted to use BILINEAR interpolation here
                 * because it takes 1/3rd the time that the BICUBIC
                 * interpolation does, however, when scaling large images down
                 * to most sizes bigger than a thumbnail we witnessed noticeable
                 * "softening" in the resultant image with BILINEAR that would
                 * be unexpectedly annoying to a user expecting a "QUALITY"
                 * scale of their original image. Instead BICUBIC was chosen to
                 * honor the contract of a QUALITY scale of the original image.
                 */
                result = scaleImageIncrementally(src, targetWidth, targetHeight, scalingMethod,
                        RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            }
        }

        if (DEBUG) {
            log(0, "Resized Image in %d ms", System.currentTimeMillis() - t);
        }

        // Apply any optional operations (if specified).
        if (ops != null && ops.length > 0) {
            result = apply(result, ops);
        }
        return result;
    }

    public static BufferedImage rotate(BufferedImage src, Rotation rotation, BufferedImageOp... ops) {
        long t = -1;
        if (DEBUG) {
            t = System.currentTimeMillis();
        }
        if (null == src) {
            throw new IllegalArgumentException("src cannot be null");
        }
        if (null == rotation) {
            throw new IllegalArgumentException("rotation cannot be null");
        }
        if (DEBUG) {
            log(0, "Rotating Image [%s]...", rotation);
        }
        int newWidth = src.getWidth();
        int newHeight = src.getHeight();
        AffineTransform tx = new AffineTransform();
        switch (rotation) {
            case CW_90:
                newWidth = src.getHeight();
                newHeight = src.getWidth();
                tx.translate(newWidth, 0);
                tx.quadrantRotate(1);

                break;
            case CW_270:
                newWidth = src.getHeight();
                newHeight = src.getWidth();
                tx.translate(0, newHeight);
                tx.quadrantRotate(3);
                break;
            case CW_180:
                tx.translate(newWidth, newHeight);
                tx.quadrantRotate(2);
                break;
            case FLIP_HORZ:
                tx.translate(newWidth, 0);
                tx.scale(-1.0, 1.0);
                break;
            case FLIP_VERT:
                tx.translate(0, newHeight);
                tx.scale(1.0, -1.0);
                break;
            default:
        }
        BufferedImage result = createOptimalImage(src, newWidth, newHeight);
        Graphics2D g2d = (Graphics2D) result.createGraphics();
        g2d.drawImage(src, tx, null);
        g2d.dispose();
        if (DEBUG) {
            log(0, "Rotation Applied in %d ms, result [width=%d, height=%d]",
                    System.currentTimeMillis() - t, result.getWidth(), result.getHeight());
        }

        // Apply any optional operations (if specified).
        if (ops != null && ops.length > 0) {
            result = apply(result, ops);
        }
        return result;
    }

    protected static Method determineScalingMethod(int targetWidth, int targetHeight, float ratio) {
        // Get the primary dimension based on the orientation of the image
        int length = (ratio <= 1 ? targetWidth : targetHeight);

        // Default to speed
        Method result = Method.SPEED;

        // Figure out which scalingMethod should be used
        if (length <= Scalr.THRESHOLD_QUALITY_BALANCED) {
            result = Method.QUALITY;
        } else if (length <= Scalr.THRESHOLD_BALANCED_SPEED) {
            result = Method.BALANCED;
        }
        if (DEBUG) {
            log(2, "AUTOMATIC scaling method selected: %s", result.name());
        }
        return result;
    }

    protected static BufferedImage scaleImage(BufferedImage src, int targetWidth, int targetHeight,
                                              Object interpolationHintValue) {
        // Setup the rendering resources to match the source image's
        BufferedImage result = createOptimalImage(src, targetWidth, targetHeight);
        Graphics2D resultGraphics = result.createGraphics();
        // Scale the image to the new buffer using the specified rendering hint.
        resultGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationHintValue);
        resultGraphics.drawImage(src, 0, 0, targetWidth, targetHeight, null);
        // Just to be clean, explicitly dispose our temporary graphics object
        resultGraphics.dispose();
        //返回缩放后的图片
        return result;
    }

    protected static BufferedImage scaleImageIncrementally(BufferedImage src, int targetWidth, int targetHeight,
                                                           Method scalingMethod, Object interpolationHintValue) {
        boolean hasReassignedSrc = false;
        int incrementCount = 0;
        int currentWidth = src.getWidth();
        int currentHeight = src.getHeight();

        /*
         * The original QUALITY mode, representing Chris Campbell's algorithm,
         * is to step down by 1/2s every time when scaling the image
         * incrementally. Users pointed out that using this method to scale
         * images with noticeable straight lines left them really jagged in
         * smaller thumbnail format.
         *
         * After investigation it was discovered that scaling incrementally by
         * smaller increments was the ONLY way to make the thumbnail sized
         * images look less jagged and more accurate; almost matching the
         * accuracy of Mac's built in thumbnail generation which is the highest
         * quality resize I've come across (better than GIMP Lanczos3 and
         * Windows 7).
         *
         * A divisor of 7 was chose as using 5 still left some jaggedness in the
         * image while a divisor of 8 or higher made the resulting thumbnail too
         * soft; like our OP_ANTIALIAS convolve op had been forcibly applied to
         * the result even if the user didn't want it that soft.
         *
         * Using a divisor of 7 for the ULTRA_QUALITY seemed to be the sweet
         * spot.
         *
         * NOTE: Below when the actual fraction is used to calculate the small
         * portion to subtract from the current dimension, this is a
         * progressively smaller and smaller chunk. When the code was changed to
         * do a linear reduction of the image of equal steps for each
         * incremental resize (e.g. say 50px each time) the result was
         * significantly worse than the progressive approach used below; even
         * when a very high number of incremental steps (13) was tested.
         */
        int fraction = (scalingMethod == Method.ULTRA_QUALITY ? 7 : 2);

        do {
            int prevCurrentWidth = currentWidth;
            int prevCurrentHeight = currentHeight;

            /*
             * If the current width is bigger than our target, cut it in half
             * and sample again.
             */
            if (currentWidth > targetWidth) {
                currentWidth -= (currentWidth / fraction);
                /*
                 * If we cut the width too far it means we are on our last
                 * iteration. Just set it to the target width and finish up.
                 */
                if (currentWidth < targetWidth) {
                    currentWidth = targetWidth;
                }
            }

            /*
             * If the current height is bigger than our target, cut it in half
             * and sample again.
             */

            if (currentHeight > targetHeight) {
                currentHeight -= (currentHeight / fraction);
                /*
                 * If we cut the height too far it means we are on our last
                 * iteration. Just set it to the target height and finish up.
                 */

                if (currentHeight < targetHeight) {
                    currentHeight = targetHeight;
                }
            }

            /*
             * Stop when we cannot incrementally step down anymore.
             *
             * This used to use a || condition, but that would cause problems
             * when using FIT_EXACT such that sometimes the width OR height
             * would not change between iterations, but the other dimension
             * would (e.g. resizing 500x500 to 500x250).
             *
             * Now changing this to an && condition requires that both
             * dimensions do not change between a resize iteration before we
             * consider ourselves done.
             */
            if (prevCurrentWidth == currentWidth && prevCurrentHeight == currentHeight) {
                break;
            }
            if (DEBUG) {
                log(2, "Scaling from [%d x %d] to [%d x %d]", prevCurrentWidth,
                        prevCurrentHeight, currentWidth, currentHeight);
            }

            // Render the incremental scaled image.
            BufferedImage incrementalImage = scaleImage(src, currentWidth, currentHeight, interpolationHintValue);

            /*
             * Before re-assigning our interim (partially scaled)
             * incrementalImage to be the new src image before we iterate around
             * again to process it down further, we want to flush() the previous
             * src image IF (and only IF) it was one of our own temporary
             * BufferedImages created during this incremental down-sampling
             * cycle. If it wasn't one of ours, then it was the original
             * caller-supplied BufferedImage in which case we don't want to
             * flush() it and just leave it alone.
             */
            if (hasReassignedSrc) {
                src.flush();
            }

            /*
             * Now treat our incremental partially scaled image as the src image
             * and cycle through our loop again to do another incremental
             * scaling of it (if necessary).
             */
            src = incrementalImage;

            /*
             * Keep track of us re-assigning the original caller-supplied source
             * image with one of our interim BufferedImages so we know when to
             * explicitly flush the interim "src" on the next cycle through.
             */
            hasReassignedSrc = true;

            // Track how many times we go through this cycle to scale the image.
            incrementCount++;
        } while (currentWidth != targetWidth || currentHeight != targetHeight);

        if (DEBUG) {
            log(2, "Incrementally Scaled Image in %d steps.", incrementCount);
        }

        /*
         * Once the loop has exited, the src image argument is now our scaled
         * result image that we want to return.
         */
        return src;
    }


    protected static BufferedImage copyToOptimalImage(BufferedImage src) {
        if (null == src) {
            throw new IllegalArgumentException("src cannot be null");
        }
        int type = (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB
                : BufferedImage.TYPE_INT_ARGB);
        BufferedImage result = new BufferedImage(src.getWidth(), src.getHeight(), type);
        Graphics g = result.getGraphics();
        g.drawImage(src, 0, 0, null);
        g.dispose();

        return result;
    }

    protected static BufferedImage createOptimalImage(BufferedImage src) {
        return createOptimalImage(src, src.getWidth(), src.getHeight());
    }

    protected static BufferedImage createOptimalImage(BufferedImage src, int width, int height)
            throws IllegalArgumentException {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("width [" + width + "] and height [" + height + "] must be > 0");
        }
        return new BufferedImage(width, height, (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB
                : BufferedImage.TYPE_INT_ARGB));
    }

    protected static void log(int depth, String message, Object... params) {
        if (DEBUG) {
            System.out.print(Scalr.LOG_PREFIX);
            for (int i = 0; i < depth; i++) {
                System.out.print("\t");
            }
            System.out.printf(message, params);
            System.out.println();
        }
    }

    /**
     * 表明使用不同的缩放算法
     */
    public static enum Method {
        /**
         * 自动选择
         */
        AUTOMATIC,
        /**
         * 速度优先
         */
        SPEED,
        /**
         * 质量与速度平衡
         */
        BALANCED,
        /**
         * 质量优先
         */
        QUALITY,
        /**
         * 质量极好的
         */
        ULTRA_QUALITY;
    }

    /**
     * 表明算法使用的缩放模型
     */
    public static enum Mode {
        /**
         * 表明缩放图片的比例尺寸以源图尺寸为标准
         */
        AUTOMATIC,
        /**
         * 以固定尺寸缩放
         */
        FIT_EXACT,
        /**
         * 用于指示缩放实现应计算适合边框的最大图像的尺寸，不进行剪裁或变形，保留原始比例
         */
        BEST_FIT_BOTH,
        /**
         * 以图像的宽为基准进行处理
         */
        FIT_TO_WIDTH,
        /**
         * 以图像的高为基准进行处理
         */
        FIT_TO_HEIGHT;
    }

    /**
     * 定义不同的旋转
     */
    public static enum Rotation {
        /**
         * 顺时针90度
         */
        CW_90,
        /**
         * 顺时针180度
         */
        CW_180,
        /**
         * 顺时针270度
         */
        CW_270,
        /**
         * 水平翻转图片，沿y轴
         */
        FLIP_HORZ,
        /**
         * 垂直翻转图片，沿x轴
         */
        FLIP_VERT;
    }

}
